<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="IEA2r" id="IEA2r"><span data-lake-id="u29edee8b" id="u29edee8b">典型回答</span></h1>
  <p data-lake-id="u1cb03346" id="u1cb03346"><br></p>
  <p data-lake-id="u7856889e" id="u7856889e"><span data-lake-id="u663402d2" id="u663402d2">为了避免Redis实现的分布式锁超时，Redisson中引入了watch dog的机制，他可以帮助我们在Redisson实例被关闭前，不断的延长锁的有效期。</span></p>
  <p data-lake-id="uffe12923" id="uffe12923"><span data-lake-id="u30f629e5" id="u30f629e5">​</span><br></p>
  <p data-lake-id="ufda3fa51" id="ufda3fa51"><span data-lake-id="uf75c8943" id="uf75c8943">那么，它是如何实现的呢？</span></p>
  <p data-lake-id="ucd6de4e1" id="ucd6de4e1"><span data-lake-id="u2f10c788" id="u2f10c788">​</span><br></p>
  <p data-lake-id="udb3ed82b" id="udb3ed82b"><span data-lake-id="u43e244c2" id="u43e244c2">在Redisson中，watch dog的主要实现在</span><a href="https://github.com/redisson/redisson/blob/master/redisson/src/main/java/org/redisson/RedissonBaseLock.java#L155" target="_blank" data-lake-id="u64db67f8" id="u64db67f8"><span data-lake-id="ud0131dbb" id="ud0131dbb">scheduleExpirationRenewal</span></a><span data-lake-id="u3c4e4ac0" id="u3c4e4ac0">方法中：</span></p>
  <p data-lake-id="u0acbfce7" id="u0acbfce7"><span data-lake-id="u277e3c55" id="u277e3c55">​</span><br></p>
  <pre lang="java"><code>
protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        try {
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

//定时任务执行续期
private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = getServiceManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            CompletionStage&lt;Boolean&gt; future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -&gt; {
                if (e != null) {
                    log.error("Can't update lock {} expiration", getRawName(), e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                
                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}


//使用LUA脚本，进行续期
protected CompletionStage&lt;Boolean&gt; renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}
</code></pre>
  <p data-lake-id="u8ab4ec77" id="u8ab4ec77"><br></p>
  <p data-lake-id="uc3b8b4ee" id="uc3b8b4ee"><span data-lake-id="ub792fa39" id="ub792fa39">可以看到，上面的代码的主要逻辑就是用了一个TimerTask来实现了一个定时任务，设置了</span><code data-lake-id="u31851963" id="u31851963"><span data-lake-id="ude055133" id="ude055133">internalLockLeaseTime / 3</span></code><span data-lake-id="u9babf7f0" id="u9babf7f0">的时长进行一次锁续期。默认的超时时长是30s，那么他会每10s进行一次续期，通过LUA脚本进行续期，再续30s</span></p>
  <p data-lake-id="u52ca9f20" id="u52ca9f20"><br></p>
  <p data-lake-id="u941667bd" id="u941667bd"><span data-lake-id="u29e4f4fa" id="u29e4f4fa">不过，这个续期也不是无脑续，他也是有条件的，其中</span><code data-lake-id="ud007e905" id="ud007e905"><span data-lake-id="ubc55de78" id="ubc55de78">ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());</span></code><span data-lake-id="u4cfe69bf" id="u4cfe69bf">这个值得我们关注，他会从</span><code data-lake-id="u4c565768" id="u4c565768"><span data-lake-id="u94379788" id="u94379788">EXPIRATION_RENEWAL_MAP</span></code><span data-lake-id="u5143ad3f" id="u5143ad3f">中尝试获取一个KV对，如果查不到，就不续期了。</span></p>
  <p data-lake-id="u9a733fb4" id="u9a733fb4"><span data-lake-id="ud74f8736" id="ud74f8736">​</span><br></p>
  <p data-lake-id="u854c57f4" id="u854c57f4"><code data-lake-id="u7f943001" id="u7f943001"><span data-lake-id="u74218b54" id="u74218b54">EXPIRATION_RENEWAL_MAP</span></code><span data-lake-id="u3848d783" id="u3848d783">这个东西，会在unlock的时候操作的，对他进行remove，所以一个锁如果被解了，那么就不会再继续续期了：</span></p>
  <p data-lake-id="uc7d188b9" id="uc7d188b9"><span data-lake-id="u96b234a5" id="u96b234a5">​</span><br></p>
  <pre lang="java"><code>
@Override
public void unlock() {
    try {
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        } else {
            throw e;
        }
    }
}

@Override
public RFuture&lt;Void&gt; unlockAsync(long threadId) {
    return getServiceManager().execute(() -&gt; unlockAsync0(threadId));
}

private RFuture&lt;Void&gt; unlockAsync0(long threadId) {
    CompletionStage&lt;Boolean&gt; future = unlockInnerAsync(threadId);
    CompletionStage&lt;Void&gt; f = future.handle((opStatus, e) -&gt; {
        cancelExpirationRenewal(threadId);

        if (e != null) {
            if (e instanceof CompletionException) {
                throw (CompletionException) e;
            }
            throw new CompletionException(e);
        }
        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            throw new CompletionException(cause);
        }

        return null;
    });

    return new CompletableFutureWrapper&lt;&gt;(f);
}

protected void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}
</code></pre>
  <p data-lake-id="udb3906d3" id="udb3906d3"><br></p>
  <p data-lake-id="u6ded2bf7" id="u6ded2bf7"><span data-lake-id="u955a7e72" id="u955a7e72">以上代码，第4行-&gt;16行-&gt;22行-&gt;57行。就是一次unlock过程中，对EXPIRATION_RENEWAL_MAP进行移除，进而取消下一次锁续期的实现细节。</span></p>
  <p data-lake-id="uae364f73" id="uae364f73"><span data-lake-id="u63766988" id="u63766988">​</span><br></p>
  <p data-lake-id="uf6f425ba" id="uf6f425ba"><span data-lake-id="u08db3a56" id="u08db3a56">并且在unlockAsync方法中，不管unlockInnerAsync是否执行成功，还是抛了异常，都不影响cancelExpirationRenewal的执行，也可以理解为，只要unlock方法被调用了，即使解锁未成功，那么也可以停止下一次的锁续期。</span></p>
  <p data-lake-id="u9adef0da" id="u9adef0da"><span data-lake-id="u36b200d9" id="u36b200d9">​</span><br></p>
  <h1 data-lake-id="Tn5AQ" id="Tn5AQ"><span data-lake-id="u817ddfeb" id="u817ddfeb">扩展知识</span></h1>
  <p data-lake-id="u0ac270fa" id="u0ac270fa"><br></p>
  <h2 data-lake-id="wW6OI" id="wW6OI"><span data-lake-id="u1b31ecd6" id="u1b31ecd6">什么情况会进行续期</span></h2>
  <p data-lake-id="udb925d63" id="udb925d63"><br></p>
  <p data-lake-id="u940b9615" id="u940b9615"><span data-lake-id="u9f75be08" id="u9f75be08">当我们使用Redisson创建一个分布式锁的时候，并不是所有情况都会续期的，我们可以看下以下加锁过程的代码实现：</span></p>
  <p data-lake-id="u2628bf19" id="u2628bf19"><span data-lake-id="ub3ba5f3f" id="ub3ba5f3f">​</span><br></p>
  <pre lang="java"><code>
private RFuture&lt;Long&gt; tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture&lt;Long&gt; ttlRemainingFuture;
    if (leaseTime &gt; 0) {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage&lt;Long&gt; s = handleNoSync(threadId, ttlRemainingFuture);
    ttlRemainingFuture = new CompletableFutureWrapper&lt;&gt;(s);

    CompletionStage&lt;Long&gt; f = ttlRemainingFuture.thenApply(ttlRemaining -&gt; {
        // lock acquired
        if (ttlRemaining == null) {
            if (leaseTime &gt; 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper&lt;&gt;(f);
}

</code></pre>
  <p data-lake-id="ub263ebba" id="ub263ebba"><br></p>
  <p data-lake-id="u4dc20d82" id="u4dc20d82"><span data-lake-id="u494f8b98" id="u494f8b98">注意看第15-19行，只有当leaseTime &lt;= 0的时候，Redisson才会进行续期，所以，当我们加锁时，如果指定了超时时间，那么是不会被续期的。</span></p>
  <p data-lake-id="u586b3065" id="u586b3065"><span data-lake-id="ucc20eed1" id="ucc20eed1">​</span><br></p>
  <h2 data-lake-id="eLnRb" id="eLnRb"><span data-lake-id="u324f7452" id="u324f7452">什么情况会停止续期</span></h2>
  <p data-lake-id="ubadfd447" id="ubadfd447"><br></p>
  <p data-lake-id="u24c20492" id="u24c20492"><span data-lake-id="u93b818b8" id="u93b818b8">首先，就是我们上面讲过的那种，如果一个锁的unlock方法被调用了，那么就会停止续期。</span></p>
  <p data-lake-id="ue21dcf43" id="ue21dcf43"><span data-lake-id="u7a35a31e" id="u7a35a31e">​</span><br></p>
  <p data-lake-id="ubc0a49be" id="ubc0a49be"><span data-lake-id="ub7e1d057" id="ub7e1d057">那么，取消续期的核心代码如下：</span></p>
  <p data-lake-id="ubc370a37" id="ubc370a37"><span data-lake-id="uc77e738e" id="uc77e738e">​</span><br></p>
  <pre lang="java"><code>
protected void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}
</code></pre>
  <p data-lake-id="ua61ce7f4" id="ua61ce7f4"><span data-lake-id="u1e10682c" id="u1e10682c">​</span><br></p>
  <p data-lake-id="uc20624e8" id="uc20624e8"><span data-lake-id="u4849636e" id="u4849636e">主要就是通过EXPIRATION_RENEWAL_MAP.remove来做的。那么cancelExpirationRenewal还有下面一处调用：</span></p>
  <p data-lake-id="u869fe613" id="u869fe613"><span data-lake-id="u3804d375" id="u3804d375">​</span><br></p>
  <pre lang="java"><code>
protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        try {
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}
</code></pre>
  <p data-lake-id="u374860d1" id="u374860d1"><span data-lake-id="u5d79b3c5" id="u5d79b3c5">​</span><br></p>
  <p data-lake-id="u8bc3961b" id="u8bc3961b"><span data-lake-id="ud1c76d98" id="ud1c76d98">也就是说，在尝试开启续期的过程中，如果线程被中断了，那么就会取消续期动作了。</span></p>
  <p data-lake-id="u067f779b" id="u067f779b"><span data-lake-id="uea9162cb" id="uea9162cb">​</span><br></p>
  <p data-lake-id="u962a36ee" id="u962a36ee"><span data-lake-id="ub650c9d5" id="ub650c9d5">目前，Redisson是没有针对最大续期次数和最大续期时间的支持的。所以，正常情况下，如果没有解锁，是会一直续期下去的。</span></p>
  <p data-lake-id="uf285f30f" id="uf285f30f"><span data-lake-id="ua22fe89f" id="ua22fe89f">​</span><br></p>
  <p data-lake-id="u4e9f6a10" id="u4e9f6a10"><span data-lake-id="ud56c6675" id="ud56c6675">但是需要注意的是，Redisson的续期是Netty的时间轮（TimerTask、Timeout、Timer）的，并且操作都是基于JVM的，所以，当应用宕机、下线或者重启后，续期任务就没有了。这样也能在一定程度上避免机器挂了但是锁一直不释放导致的死锁问题。</span></p>
  <p data-lake-id="ueb557bac" id="ueb557bac"><span data-lake-id="u3f90d28f" id="u3f90d28f">​</span><br></p>
  <p data-lake-id="udea84006" id="udea84006"><br></p>
  <p data-lake-id="u30f763e4" id="u30f763e4"><br></p>
  <p data-lake-id="u8828edab" id="u8828edab"><br></p>
  <h2 data-lake-id="qnGcA" id="qnGcA"><span data-lake-id="ub4ca4823" id="ub4ca4823" style="color: rgb(38, 38, 38)">watchdog一直续期，客户端挂了怎么办？</span></h2>
 </body>
</html>